//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.ScientificWorkflow.UIDesigner.Monitor
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Drawing.Imaging;
    using System.Globalization;
    using System.IO;
    using System.Threading;
    using System.Windows.Threading;
    using Microsoft.Research.ScientificWorkflow.TridentUtilities;
    using Microsoft.Office.Core;
    using Microsoft.Practices.Unity;
    using Microsoft.Research.DataLayer;
    using Excel = Microsoft.Office.Interop.Excel;
    using PowerPoint = Microsoft.Office.Interop.PowerPoint;
    using SR = Microsoft.Research.DataLayer;
    using Word = Microsoft.Office.Interop.Word;
    using System.Linq;
    using Microsoft.Research.eResearch.Common.Linq;
    using System.Windows;

    public class DataProductService : IDataProductService
    {
        /// <summary>
        /// Folder name in the temporary folder
        /// </summary>
        private const string TRIDENT_FOLDER = "TridentDPTemp";

        /// <summary>
        /// connection object for pooling thread
        /// </summary>
        private SR.Connection poolingConnection;

        /// <summary>
        /// Thread which pools the job
        /// </summary>
        private Thread PoolJobThread;

        /// <summary>
        /// A dummy object used in a lock.
        /// </summary>
        private object lockingObject = new object();

        /// <summary>
        /// Unique Identifier for the job
        /// </summary>
        private Guid currentJobId;

        /// <summary>
        /// Holds Dispather which raises the Events
        /// </summary>
        private Dispatcher dispatcher;

        /// <summary>
        /// The number of attempts completed.
        /// </summary>
        private int connectionAttempts;

        /// <summary>
        /// The maximum reconnection attempts if the connection is not alive.
        /// </summary>
        private int maxConnectionAttempts = 5;

        /// <summary>
        /// Current instance of Job.
        /// </summary>
        private SR.Job currentJob;

        /// <summary>
        /// Event hander for dataproductreceived
        /// </summary>
        public event EventHandler<DataProductEventArgs> DataProductReceived;

        /// <summary>
        /// Event hander for Update Status of the executing job
        /// </summary>
        public event EventHandler<JobStatusEventArgs> UpdateStatus;

        /// <summary>
        /// Raised whenever datamodel needs to be cleared
        /// </summary>
        public event EventHandler Clear;

        /// <summary>
        /// Gets or sets the polling connection.
        /// </summary>
        /// <value>The polling connection.</value>
        public Connection PollingConnection
        {
            get
            {
                return this.poolingConnection;
            }
            set
            {
                this.poolingConnection = value;
            }
        }

        /// <summary>
        /// The maximum reconnection attempts if the connection is not alive.
        /// </summary>
        public int MaxRegistryConnectionAttempts
        {
            get
            {
                return this.maxConnectionAttempts;
            }
            set
            {
                this.maxConnectionAttempts = value;
            }
        }

        /// <summary>
        /// Raises ClearEvent
        /// </summary>
        public void ClearModel()
        {
            if (Clear != null)
            {
                this.Clear(this, new EventArgs());
            }
        }

        /// <summary>
        /// Constructor
        /// </summary>
        public DataProductService()
        {
            this.dispatcher = System.Windows.Application.Current.Dispatcher;
        }

        /// <summary>
        /// Initiate the Data Product Service Thread
        /// </summary>
        /// <param name="jobId">Guid</param>
        /// <param name="poolConnection">Connection</param>
        public void StartDataProductService(Guid jobId, Connection poolConnection)
        {
            this.currentJobId = jobId;
            this.poolingConnection = poolConnection;
            this.ClearTempData();

            this.HaltService();

            this.PoolJobThread = new Thread(this.PoolJobExecutionStatus);
            this.PoolJobThread.Start();
        }

        /// <summary>
        /// Clears all the temporary data.
        /// </summary>
        public void ClearTempData()
        {
            DataProductService.ClearTemporaryDataProductsFolder();
        }

        /// <summary>
        /// Stop the pooling thread
        /// </summary>
        public void HaltService()
        {
            this.currentJob = null;
            if (this.PoolJobThread != null && this.PoolJobThread.IsAlive)
            {
                try
                {
                    this.PoolJobThread.Abort();
                }
                catch (Exception)
                {
                }
            }
        }

        /// <summary>
        /// Delegete which raises Update Status Event
        /// </summary>
        /// <param name="sender">object</param>
        /// <param name="e">JobStatusEventArgs</param>
        private void RaiseUpdateStatus(object sender, JobStatusEventArgs e)
        {
            if (this.UpdateStatus != null)
            {
                this.UpdateStatus(sender, e);
            }
        }

        /// <summary>
        /// Delegete which raises DataProductReceived Event when it receives DataProduct
        /// </summary>
        /// <param name="sender">object</param>
        /// <param name="e">DataProductEventArgs</param>
        private void RaiseDataProduct(object sender, DataProductEventArgs e)
        {
            if (this.DataProductReceived != null)
            {
                DataProductReceived(sender, e);
            }
        }

        /// <summary>
        /// Fetches the temp path, which will be 'Temp folder of system\TridentDpTemp\CurrentApplicationName'.
        /// </summary>
        /// <returns>
        /// App specific temporary path.
        /// </returns>
        private static string FetchTempPath()
        {
            string tempFolder = Path.Combine(Path.GetTempPath(), TRIDENT_FOLDER);
            if (!Directory.Exists(tempFolder))
            {
                Directory.CreateDirectory(tempFolder);
            }

            tempFolder = Path.Combine(tempFolder, Application.Current.ToString());
            if (!Directory.Exists(tempFolder))
            {
                Directory.CreateDirectory(tempFolder);
            }

            return tempFolder;
        }

        /// <summary>
        /// Converts the data product content to browsable format file
        /// </summary>
        /// <param name="dataProduct">DataProduct object.</param>
        /// <returns>Temp file path of data product.</returns>
        private static string GenerateWebBrowserFile(DataProduct dataProduct)
        {
            string tempFolder = DataProductService.FetchTempPath();
            ImageFormat imageFormat = ImageFormat.Png;

            if (!Directory.Exists(tempFolder))
            {
                Directory.CreateDirectory(tempFolder);
            }

            string fileExtenstion = string.Empty;

            //// MIME type to file extension mapping
            switch (dataProduct.Type)
            {
                case "image/png":
                    fileExtenstion = ".png";
                    imageFormat = ImageFormat.Png;
                    break;
                case "image/jpg":
                    fileExtenstion = ".jpg";
                    imageFormat = ImageFormat.Jpeg;
                    break;
                case "image/jpeg":
                    fileExtenstion = ".jpeg";
                    imageFormat = ImageFormat.Jpeg;
                    break;
                case "image/gif":
                    fileExtenstion = ".gif";
                    imageFormat = ImageFormat.Gif;
                    break;
                case "image/bmp":
                    fileExtenstion = ".bmp";
                    imageFormat = ImageFormat.Bmp;
                    break;
                case "text/html":
                    fileExtenstion = ".html";
                    break;
                case "text/plain":
                    fileExtenstion = ".txt";
                    break;
                case "application/ms-excel":
                    fileExtenstion = ".xlsx";
                    break;
                case "application/ms-word":
                    fileExtenstion = ".docx";
                    break;
                case "application/xml":
                    fileExtenstion = ".xml";
                    break;
                case "application/csv":
                    fileExtenstion = ".csv";
                    break;
                default:
                    fileExtenstion = string.Empty;
                    break;
            }

            string sourceFilePath = Path.Combine(tempFolder, string.Format(CultureInfo.InvariantCulture, "{0}.{1}", ((IObject)dataProduct).ID.ToString("N"), fileExtenstion));
            string htmlFilePath = Path.Combine(tempFolder, string.Format(CultureInfo.InvariantCulture, "{0}.{1}", ((IObject)dataProduct).ID.ToString("N"), "html"));

            if (File.Exists(sourceFilePath))
            {
                //// file is already converted and available. So do not generate the file again.
                return File.Exists(htmlFilePath) ? htmlFilePath : sourceFilePath;
            }

            //// save the data product content as source file
            switch (dataProduct.Type)
            {
                case "image/jpg":
                case "image/jpeg":
                case "image/png":
                case "image/bmp":
                case "image/gif":
                    dataProduct.ContentsAsImage.Save(sourceFilePath, imageFormat);
                    break;
                case "application/ms-excel":
                case "application/ms-word":
                case "application/csv":
                    File.WriteAllBytes(sourceFilePath, dataProduct.Contents);
                    break;
                case "text/plain":
                case "text/html":
                case "application/xml":
                    using (StreamWriter sw = new StreamWriter(sourceFilePath))
                    {
                        sw.WriteLine(dataProduct.ContentsAsString);
                        sw.Close();
                    }
                    break;
                default:
                    File.WriteAllBytes(sourceFilePath, dataProduct.Contents);
                    break;
            }

            //// generate HTML file for the data product
            htmlFilePath = GenerateBrowsableFormat(sourceFilePath, htmlFilePath);

            return htmlFilePath;
        }

        /// <summary>
        /// Convert the source file to html format file.
        /// </summary>
        /// <param name="source">Source file path</param>
        /// <param name="target">HTML file path</param>
        /// <returns>target path</returns>
        private static string GenerateBrowsableFormat(string source, string target)
        {
            FileInfo file = new FileInfo(source);

            switch (file.Extension)
            {
                case ".doc":
                case ".docx":
                    ConvertWordToHtml(source, target);
                    break;
                case ".csv":
                case ".xls":
                case ".xlsx":
                    ConvertExcelToHtml(source, target);
                    break;
                case ".ppt":
                case ".pptx":
                    ConvertPowerPointToHtml(source, target);
                    break;
                case ".bmp":
                case ".png":
                case ".jpg":
                case ".gif":
                case ".jpeg":
                    ConvertImageToHtml(source, target);
                    break;

                case ".xml":
                case ".html":
                case ".txt":
                    target = source;
                    break;
                default:
                    ConvertOtherFormatToHtml(source, target);
                    break;
            }

            return target;
        }

        /// <summary>
        /// Convert word file to html format file. 
        /// </summary>
        /// <param name="source">original file</param>
        /// <param name="target">html file embeding the source</param>
        private static void ConvertWordToHtml(object source, object target)
        {
            //// Creating the instance of Word Application
            Word._Application newApp = new Word.Application();

            //// Use for the parameter whose type are not known or say Missing
            object unknown = System.Type.Missing;

            //// Source document open here
            //// Additional Parameters are not known so that are  
            //// set as a missing type
            newApp.Documents.Open(ref source, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown);

            //// Specifying the format in which you want the output file 
            object format = Word.WdSaveFormat.wdFormatHTML;

            //// Changing the format of the document
            newApp.ActiveDocument.SaveAs(ref target, ref format, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown, ref unknown);

            newApp.Quit(ref unknown, ref unknown, ref unknown);

            Thread.Sleep(TimeSpan.FromSeconds(2));
        }

        /// <summary>
        /// Convert excel file to html format file.
        /// </summary>
        /// <param name="source">original file</param>
        /// <param name="target">html file embeding the source</param>
        private static void ConvertExcelToHtml(string source, string target)
        {
            //// Creating the instance of Word Application
            Excel._Application newApp = new Excel.Application();

            //// Use for the parameter whose type are not known or say Missing
            object unknown = System.Type.Missing;

            //// Source document open here
            //// Additional Parameters are not known so that are  
            //// set as a missing type
            newApp.Workbooks.Open(source, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown);

            //// Specifying the format in which you want the output file 
            object format = Excel.XlFileFormat.xlHtml;

            //// Changing the format of the document
            newApp.ActiveWorkbook.SaveAs(target, format, unknown, unknown, unknown, unknown, Excel.XlSaveAsAccessMode.xlNoChange, unknown, unknown, unknown, unknown, unknown);

            //// for closing the application
            newApp.Quit();
            Thread.Sleep(TimeSpan.FromSeconds(2));
        }

        /// <summary>
        /// Convert power point file to html format file. 
        /// </summary>
        /// <param name="source">original file</param>
        /// <param name="target">html file embeding the source</param>
        private static void ConvertPowerPointToHtml(string source, string target)
        {
            //// Creating the instance of Word Application
            PowerPoint._Application newApp = new PowerPoint.Application();
            PowerPoint._Presentation ppp;

            //// Source document open here
            //// Additional Parameters are not known so that are  
            //// set as a missing type
            ppp = newApp.Presentations.Open(source, 0, 0, MsoTriState.msoFalse);

            //// Changing the format of the document
            ppp.SaveAs(target, PowerPoint.PpSaveAsFileType.ppSaveAsHTML, MsoTriState.msoCTrue);

            //// for closing the application
            newApp.Quit();
            Thread.Sleep(TimeSpan.FromSeconds(2));
        }

        /// <summary>
        /// Convert image file to html format file. 
        /// </summary>
        /// <param name="source">original file</param>
        /// <param name="target">html file embeding the source</param>
        private static void ConvertImageToHtml(string source, string target)
        {
            using (StreamWriter file = new StreamWriter(target))
            {
                file.WriteLine(@"<html><head /><body><img src='{0}' /></body></html>", source);
                file.Close();
            }
        }

        /// <summary>
        /// Convert other file to html format file. 
        /// </summary>
        /// <param name="source">original file</param>
        /// <param name="target">html file embeding the source</param>
        private static void ConvertOtherFormatToHtml(string source, string target)
        {
            using (StreamWriter file = new StreamWriter(target))
            {
                file.WriteLine(@"<html><head /><body><a href='{0}'> Click here to view the file</a></body></html>", source);
                file.Close();
            }
        }

        /// <summary>
        /// Deletes all the files generated in the temporary folder
        /// </summary>
        private static void ClearTemporaryDataProductsFolder()
        {
            string path = DataProductService.FetchTempPath();
            if (Directory.Exists(path))
            {
                try
                {
                    Directory.Delete(path, true);
                }
                catch (System.IO.IOException ioExp)
                {
                    // Ignore: The file might be in use by the provenance controls.
                    TridentErrorHandler.HandleUIException(ioExp);
                }
            }
        }

        /// <summary>
        /// Pool the job execution status
        /// </summary>
        private void PoolJobExecutionStatus()
        {
            int waitIndex = 0;

            while (true)
            {
                try
                {
                    lock (this.lockingObject)
                    {
                        this.ReOpenRegistryConnection();

                        currentJob = SR.Job.Load(this.currentJobId, this.poolingConnection);

                        JobStatusEventArgs jobStatusArg = (currentJob.Status == SR.JobStatus.Aborted) ?
                            new JobStatusEventArgs(currentJob.Name, currentJob.Status, currentJob.ErrorMessage) :
                            new JobStatusEventArgs(currentJob.Name, currentJob.Status);

                        this.dispatcher.BeginInvoke(new EventHandler<JobStatusEventArgs>(RaiseUpdateStatus), DispatcherPriority.Normal, new object[] { this, jobStatusArg });

                        if (currentJob.Status == JobStatus.Aborted || currentJob.Status == JobStatus.Completed)
                        {
                            this.PoolJobThread.Abort();
                            break;
                        }
                    }
                }
                catch (ThreadAbortException)
                {
                }
                catch (ConnectionFailure ex)
                {
                    TridentErrorHandler.HandleUIException(ex);

                    // If reconnection fails more than the max reconnection value then exit.
                    if (this.connectionAttempts >= this.maxConnectionAttempts)
                    {
                        break;
                    }
                    this.connectionAttempts++;
                }
                catch (BackendStorageException ex)
                {
                    TridentErrorHandler.HandleUIException(ex);

                    // If reconnection fails 5 times then exit.
                    if (this.connectionAttempts >= this.maxConnectionAttempts)
                    {
                        break;
                    }
                    this.connectionAttempts++;
                }
                catch (Exception ex)
                {
                    TridentErrorHandler.HandleUIException(ex);
                    break;
                }

                try
                {
                    Thread.Sleep(500);

                    // If job is in waiting state, every 3 min show the user retry message.
                    // (waitIndex == 360) Specifes 3 min. 
                    if (currentJob.Status == JobStatus.Waiting && waitIndex == 360)
                    {
                        waitIndex = 0;

                        JobStatusEventArgs jobStatusArg = new JobStatusEventArgs(this.currentJob.Name, this.currentJob.Status, TridentResourceManager.GetString("JobTerminatedByUserMessage"));
                        this.dispatcher.BeginInvoke(new EventHandler<JobStatusEventArgs>(RaiseUpdateStatus), DispatcherPriority.Normal, new object[] { this, jobStatusArg });
                    }
                }
                catch (Exception ex)
                {
                    TridentErrorHandler.HandleUIException(ex);
                }

                waitIndex++;
            }
        }

        /// <summary>
        /// Retreive the dataproduct created. Creates a new thread which gets the dataproduct from the registry.
        /// </summary>
        /// <param name="dataProductId">The ID of the dataproduct created.</param>
        public void GetDataProductInfo(Guid dataProductId, ObservableCollection<DataProductItem> dataProducts)
        {
            if (dataProductId.Equals(Guid.Empty))
            {
                throw new ArgumentNullException("dataProductId");
            }

            Thread dataproductRetriever = null;
            try
            {
                DataproductInfo info = new DataproductInfo() { DataproductId = dataProductId, DataProducts = dataProducts };

                // Start a new thread which loads the dataproduct.
                dataproductRetriever = new Thread(new ParameterizedThreadStart(GetDataProducts));
                dataproductRetriever.Start(info);
            }
            catch (Exception ex)
            {
                if (dataproductRetriever.IsAlive)
                {
                    dataproductRetriever.Abort();
                }
                TridentErrorHandler.HandleUIException(ex);
            }
        }

        /// <summary>
        /// The delegate which retrieves the dataproduct and adds it to the list.
        /// </summary>
        /// <param name="obj">The dataproduct info object.</param>
        private void GetDataProducts(object obj)
        {
            try
            {
                lock (this.lockingObject)
                {
                    DataproductInfo info = obj as DataproductInfo;
                    DataProduct dataproduct = DataProduct.Load(info.DataproductId, this.poolingConnection);
                    DataProductItem dataProductItem = DataProductService.GenerateDummyDataProductItem(dataproduct);

                    // Add the dataproduct to the dataproducts list.
                    this.dispatcher.BeginInvoke(() => { info.DataProducts.Add(dataProductItem); });
                }
            }
            catch (Exception ex)
            {
                TridentErrorHandler.HandleUIException(ex);
            }
        }

        /// <summary>
        /// This function is used to check and 
        /// reopen the registry connection if it is closed.
        /// </summary>
        private void ReOpenRegistryConnection()
        {

            if (null != this.poolingConnection && !this.poolingConnection.Alive)
            {
                ConnectionManager mgr = ConnectionManager.CreateForAgent(
                    ConnectionManager.CancelBehavior.ThrowException,
                    ConnectionManager.Agent.ExecutionService);

                this.poolingConnection = mgr.PickConnection(ConnectionManager.ConnectionUI.NeverShowUI);
                this.connectionAttempts = 0;
            }
        }

        /// <summary>
        /// Generates the DataProductItem for the given DataProduct
        /// </summary>
        /// <param name="dataProduct">DataProduct</param>
        /// <returns>DataProductItem</returns>
        public static DataProductItem GenerateDataProductItem(DataProduct dataProduct)
        {
            string htmlPath = "about:blank";
            try
            {
                htmlPath = GenerateWebBrowserFile(dataProduct);
            }
            catch (Exception ex)
            {
                TridentErrorHandler.HandleUIException(ex);
            }

            DataProductItem dataProductItem = new DataProductItem(dataProduct, htmlPath);
            return dataProductItem;
        }

        /// <summary>
        /// Generates the Dummy DataProductItem for the given DataProduct
        /// </summary>
        /// <param name="dataProduct">DataProduct</param>
        /// <returns>DataProductItem</returns>
        public static DataProductItem GenerateDummyDataProductItem(DataProduct dataProduct)
        {
            DataProductItem dataProductItem = null;
            string htmlPath = "about:blank";
            try
            {
                dataProductItem = new DataProductItem(dataProduct, htmlPath);
            }
            catch (Exception ex)
            {
                TridentErrorHandler.HandleUIException(ex);
            }

            return dataProductItem;
        }

        /// <summary>
        /// Generates the DataProductItem for the given DataProduct
        /// </summary>
        /// <param name="dataProductID">The data product ID.</param>
        /// <returns>DataProductItem</returns>
        public DataProductItem GenerateDataProductItem(Guid dataProductId)
        {
            DataProduct dataProduct = null;
            Connection currentConn = null;
            try
            {
                currentConn = this.poolingConnection.Clone();
                currentConn.Open();
                dataProduct = DataProduct.Load(dataProductId, currentConn);
            }
            catch (Exception ex)
            {
                TridentErrorHandler.HandleUIException(ex);
            }

            DataProductItem dataProductItem = DataProductService.GenerateDataProductItem(dataProduct);
            return dataProductItem;
        }
    }

    /// <summary>
    /// This is a class which contains information to be passed to the dataproduct retriever thread.
    /// </summary>
    class DataproductInfo
    {
        public Guid DataproductId { get; set; }
        public ObservableCollection<DataProductItem> DataProducts { get; set; }
    }
}
